package com.anlogic.sdk.utils;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.logging.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Bundle;

public class FileUtils {
	private static Logger logger = LogUtils.getLogger(FileUtils.class);
	
	public static void checkAndCreateDirectory(String dirPath) {
		if (StringUtils.isEmpty(dirPath))
			return;
		File dir = new File(dirPath);
		if (!dir.exists()) {
			dir.mkdirs();
		}
	}

	public static String toPortablePath(IResource resource) {
		return resource.getLocation().toPortableString();
	}

	public static String toPortablePath(File file) {
		if (file == null)
			return null;
		return toPortablePath(file.getAbsolutePath());
	}

	public static String toPortablePath(String absolutePath) {
		if (StringUtils.isEmpty(absolutePath))
			return absolutePath;
		Path path = new Path(absolutePath);
		return path.toPortableString();
	}
	
	public static File getParentFile(File file, int deep) {
		File parent = file;
		for (int i = 0; i < deep; i++) {
			parent = parent.getParentFile();
		}
		return parent;
	}

	public static boolean localFileExists(IFile file) {
		if (file != null && (new File(file.getLocationURI())).exists())
			return true;
		return false;
	}
	
	public static boolean checkFileExists(String filePath) {
		File file = new File(filePath);
		return file.exists();
	}
	
	public static File[] getFilesBySuffix(String parentPath, String suffix) {
		List<File> files = new ArrayList<File>();
		File parentFile = new File(parentPath);
		for (File file : parentFile.listFiles()) {
			if (file.getPath().endsWith(suffix)) {
				files.add(file);
			}
		}
		return files.toArray(new File[0]);
	}
	
	public static File getFileByName(String parentPath, String name) {
		File parentFile = new File(parentPath);
		for (File file : parentFile.listFiles()) {
			if (file.getName().equals(name)) {
				return file;
			}
		}
		return null;
	}

	public static String read(File file) {
		String ret = "";

		try {
			ret = read(new FileReader(file));
		} catch (FileNotFoundException e) {
			logger.error("Unable to read file {}", file.getName());
		}

		return ret;
	}

	public static String read(Reader in) {
		StringBuffer buffer = new StringBuffer();
		try {
			while (in.ready())
				buffer.append((char) in.read());
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			close(in);
		}
		return buffer.toString();
	}

	private static void close(Closeable c) {
		if (c != null)
			try {
				c.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
	}

	public static void write(File file, String resultString) {
		FileWriter out = null;
		try {
			out = new FileWriter(file);
			out.write(resultString);
			out.close();
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			if (out != null)
				close(out);
		}
	}

	public static void closeQuietly(Reader r) {
		if (r != null)
			try {
				r.close();
			} catch (IOException iOException) {
			}
	}

	public static void closeQuietly(Writer w) {
		if (w != null)
			try {
				w.close();
			} catch (IOException iOException) {
			}
	}

	public static void copyFile(IFile dstFile, String srcPath) throws IOException {
		File src = new File(srcPath);
		File dst = new File(dstFile.getLocation().toOSString());
		copyFile(src, dst);
	}

	public static boolean copyFile(File src, File dst) throws IOException {
		if (!src.exists())
			return false;
		InputStream in = null;
		OutputStream out = null;
		try {
			in = new FileInputStream(src);
			out = new FileOutputStream(dst);
			byte[] buf = new byte[1024];
			int len;
			while ((len = in.read(buf)) > 0)
				out.write(buf, 0, len);
		} catch (IOException e) {
			throw e;
		} finally {
			if (in != null)
				try {
					in.close();
				} catch (IOException iOException) {
				}
			if (out != null)
				try {
					out.close();
				} catch (IOException iOException) {
					logger.error("Unable to close stream.");
				}
		}
		return true;
	}

	public static void copyFolder(String sourcePath, String targetPath) throws IOException {
		File sourceFile = new File(sourcePath);
		File targetFile = new File(targetPath);

		if (!sourceFile.exists()) {
			return;
		}

		if (sourceFile.isDirectory()) {
			if (!targetFile.exists()) {
				targetFile.mkdir();
			}

			for (File child : sourceFile.listFiles()) {
				String fileName = child.getName();

				String newSourcePath = (new File(sourcePath, fileName)).getPath();
				String newTargetPath = (new File(targetPath, fileName)).getPath();
				copyFolder(newSourcePath, newTargetPath);
			}
		} else {
			FileInputStream input = null;
			FileOutputStream output = null;
			try {

				input = new FileInputStream(new File(sourcePath));
				output = new FileOutputStream(new File(targetPath));

				byte[] buffer = new byte[1024];
				int bytesRead;

				while ((bytesRead = input.read(buffer)) != -1) {
					output.write(buffer, 0, bytesRead);
				}
			} catch (IOException e) {
				throw new IOException(e);
			} finally {
				if (input != null)
					input.close();
				if (output != null)
					output.close();
			}
		}
	}

	public static String[] readLines(File file) {
		List<String> lines = new ArrayList<String>();
		try {
			BufferedReader reader = new BufferedReader(new FileReader(file));
			try {
				while (true) {
					String line = reader.readLine();
					if (line == null)
						break;
					lines.add(line);
				}
			} finally {
				reader.close();
			}
		} catch (IOException e) {
			logger.error("Unable to load properties file {}", file.getName());
		}
		return lines.toArray(new String[0]);
	}
	
	public static String getLine(File file, String regex) {
		Pattern pattern = Pattern.compile(regex);
		
		String ret = "";
		String[] lines = readLines(file);
		for (String line : lines) {
			Matcher matcher = pattern.matcher(line);
			if (matcher.find()) {
				ret = line;
				break;
			}
		}
		return ret;
	}
	
	public static String getLineContent(File file, String regex) {
		Pattern pattern = Pattern.compile(regex);
		
		String ret = "";
		String[] lines = readLines(file);
		for (String line : lines) {
			Matcher matcher = pattern.matcher(line);
			if (matcher.find()) {
				ret = matcher.group(1);
				break;
			}
		}
		return ret;
	}

	public static String getFileExtension(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1)
			return null;
		if (index == name.length() - 1)
			return "";
		return name.substring(index + 1);
	}

	public static String getFileNameWithoutExt(String name) {
		int index = name.lastIndexOf('.');
		if (index == -1)
			return name;
		if (index == 0)
			return "";
		return name.substring(0, index);
	}

	public static String getPluginPath(String pluginID) {
		String path = Platform.getBundle(pluginID).getLocation(); // reference:file:/path
		if (EnvUtils.isWindows()) {
			path = path.substring(16);
		}
		if (EnvUtils.isLinux()) {
			path = path.substring(15);
		}
		return path;
	}

	public static String getPluginResourcePath(String pluginID, String path) throws IOException {
		URL url = Platform.getBundle(pluginID).getResource(path);
		String resourcesPath = FileLocator.toFileURL(url).getPath();
		return resourcesPath;
	}

	public static Reader getPluginResourceReader(String pluginID, String relativePath) throws IOException {
		URL url = Platform.getBundle(pluginID).getResource(relativePath);
		return new InputStreamReader(url.openStream());
	}
	
	public static Reader getContentReader(AbstractUIPlugin plugin, String relativePath) throws IOException {
		if (plugin == null)
			return new FileReader(new File(relativePath));
		Bundle bundle = plugin.getBundle();
		URL url = getResourceURL(bundle, relativePath);
		UtilsPlugin.getDefault().logMsg(String.format("Resource data url:%s", new Object[] { url }));
		return new InputStreamReader(url.openStream());
	}

	private static URL getResourceURL(Bundle bundle, String relativePath) {
		return FileLocator.find(bundle, (IPath) new Path(relativePath), null);
	}

	public static boolean checkPatternMatch(Pattern pattern, String target) {
		return pattern.matcher(target).matches();
	}

	public static boolean checkFilePathLegality(String path) {
		Pattern linux_path_pattern = Pattern
				.compile("(/([a-zA-Z0-9][a-zA-Z0-9_\\-\\.]{0,255}/)*([a-zA-Z0-9][a-zA-Z0-9_\\-\\.]{0,255})|/)");
		Pattern windows_path_pattern = Pattern
				.compile("(^[A-Z]:((\\\\|/)([a-zA-Z0-9_\\-\\.]){1,255}){1,255}|([A-Z]:(\\\\|/)))");
		if (EnvUtils.isLinux())
			return checkPatternMatch(linux_path_pattern, path);
		if (EnvUtils.isWindows())
			return checkPatternMatch(windows_path_pattern, path);
		return false;
	}

	public static boolean checkFileNameLegality(String filename) {
		Pattern filename_pattern = Pattern.compile("^[a-zA-Z]([a-zA-Z0-9_\\-\\.])*$");
		return checkPatternMatch(filename_pattern, filename);
	}
	
	public static URL filePathToURL(String path) {
		URL url = null;
		try {
			url = new File(path).toURI().toURL();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return url;
	}
	
	public static URI filePathToURI(String path) {
		URI uri = new File(path).toURI();
		return uri;
	}

	public static String getRealFilePath(String path) {
		String FILE_SEPARATOR = System.getProperty("file.separator");
		return path.replace("/", FILE_SEPARATOR).replace("\\", FILE_SEPARATOR);
	}

	public static String getFileName(String filePath) {
		return new File(filePath).getName();
	}

	public static String getFileBaseName(String filePath) {
		String fileName = new File(filePath).getName();
		String baseName = fileName.substring(0, fileName.lastIndexOf("."));
		return baseName;
	}

	public static String getFileExtend(String filePath) {
		String fileName = new File(filePath).getName();
		String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
		return fileExt;
	}

	public static String concatFilePath(String pathA, String pathB) {
		String path = convertToLinuxPath(pathA) + "/" + convertToLinuxPath(pathB);
		return path;
	}

	public static String convertToLinuxPath(String path) {
		if (null == path)
			return path;
		path = path.replace('\\', '/');
		return path;
	}

	public static String convertToWinPath(String path) {
		if (null == path)
			return path;
		path = path.replace('/', '\\');
		return path;
	}
	
	public static boolean isReadable(String filePath) {
		File file = new File(filePath);
		if (file.exists() && file.canRead()) {
			return true;
		}
		return false;
	}
	
	public static boolean isWritable(String filePath) {
		File file = new File(filePath);
		if (file.exists() && file.canWrite()) {
			return true;
		}
		return false;
	}
	
	public static boolean isExecutable(String filePath) {
		File file = new File(filePath);
		if (file.exists() && file.canExecute()) {
			return true;
		}
		return false;
	}
	
	public static boolean isEncrypted(String filePath) {
	    File file = new File(filePath);
	    if (!file.exists() || !file.isFile()) {
	        return false;
	    }
	    
	    try {
	        // Read file data
	        byte[] data = Files.readAllBytes(file.toPath());
	        if (data.length == 0) return false;
	        
	        // 1. Check for known encryption file signatures/headers
	        if (hasEncryptionSignature(data)) {
	            return true;
	        }
	        
	        // 2. Check file extension
	        String extension = getFileExtension(file.getName());
	        if (isKnownEncryptedExtension(extension)) {
	            return true;
	        }
	        
	        // 3. Check for Base64 encoded encrypted content (added for AES/DES encrypted files)
	        if (isBase64EncryptedContent(data)) {
	            return true;
	        }
	        
	        // 4. Skip known binary/compressed formats that naturally have high entropy
	        if (isKnownBinaryFormat(data, extension)) {
	            return false;
	        }
	        
	        // 5. Calculate entropy
	        double entropy = calculateEntropy(data);
	        
	        // 6. Perform additional statistical tests
	        boolean highEntropy = entropy > 7.0; // Stricter threshold 7.5
	        boolean uniformDistribution = hasUniformByteDistribution(data);
	        boolean lacksTextPatterns = !hasTextPatterns(data);
	        boolean lacksCommonBinaryStructures = !hasCommonBinaryStructures(data);
	        
	        // Combine multiple indicators
	        int encryptionIndicators = 0;
	        if (highEntropy) encryptionIndicators++;
	        if (uniformDistribution) encryptionIndicators++;
	        if (lacksTextPatterns) encryptionIndicators++;
	        if (lacksCommonBinaryStructures) encryptionIndicators++;
	        
	        // Require at least 3 out of 4 indicators for small files, 2 out of 4 for larger files
	        int threshold = data.length < 1024 ? 3 : 2;
	        return encryptionIndicators >= threshold;
	        
	    } catch (IOException e) {
	        logger.error("Error reading file for encryption detection: {}", file.getName(), e);
	        return false;
	    }
	}

	/**
	 * Check for known encryption file signatures and headers
	 */
	private static boolean hasEncryptionSignature(byte[] data) {
	    if (data.length < 16) return false;
	    
	    // Check for common encryption signatures
	    String header = new String(data, 0, Math.min(16, data.length));
	    
	    // PGP encrypted files
	    if (data.length >= 3 && data[0] == (byte)0x85 && (data[1] & 0xFF) >= 0x01 && (data[1] & 0xFF) <= 0x03) {
	        return true;
	    }
	    
	    // GPG encrypted files
	    if (header.startsWith("-----BEGIN PGP MESSAGE-----")) {
	        return true;
	    }
	    
	    // TrueCrypt/VeraCrypt volumes (random header)
	    if (data.length >= 64) {
	        // Check for lack of null bytes in first 64 bytes (characteristic of encrypted headers)
	        boolean hasNullByte = false;
	        for (int i = 0; i < 64; i++) {
	            if (data[i] == 0) {
	                hasNullByte = true;
	                break;
	            }
	        }
	        if (!hasNullByte && calculateEntropy(java.util.Arrays.copyOfRange(data, 0, 64)) > 7.8) {
	            return true;
	        }
	    }
	    
	    return false;
	}

	/**
	 * Check if file extension indicates known encrypted file types
	 */
	private static boolean isKnownEncryptedExtension(String extension) {
	    if (extension == null) return false;
	    extension = extension.toLowerCase();
	    
	    String[] encryptedExtensions = {
	        "gpg", "pgp", "asc", "encrypted", "enc", "crypt", "locked", 
	        "axx", "tc", "hc", "vault", "keychain", "p12", "pfx"
	    };
	    
	    for (String ext : encryptedExtensions) {
	        if (ext.equals(extension)) {
	            return true;
	        }
	    }
	    return false;
	}

	/**
	 * Check if file is a known binary/compressed format that naturally has high entropy
	 */
	private static boolean isKnownBinaryFormat(byte[] data, String extension) {
	    if (data.length < 8) return false;
	    
	    // Check file signatures for common binary formats
	    // ZIP files
	    if (data[0] == 0x50 && data[1] == 0x4B) return true;
	    
	    // JPEG files
	    if (data[0] == (byte)0xFF && data[1] == (byte)0xD8) return true;
	    
	    // PNG files
	    if (data.length >= 8 && data[0] == (byte)0x89 && data[1] == 0x50 && 
	        data[2] == 0x4E && data[3] == 0x47) return true;
	    
	    // GIF files
	    if (data.length >= 6 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) return true;
	    
	    // PDF files
	    if (data.length >= 4 && data[0] == 0x25 && data[1] == 0x50 && 
	        data[2] == 0x44 && data[3] == 0x46) return true;
	    
	    // Check by extension
	    if (extension != null) {
	        extension = extension.toLowerCase();
	        String[] binaryExtensions = {
	            "jpg", "jpeg", "png", "gif", "bmp", "tiff", "ico",
	            "zip", "rar", "7z", "tar", "gz", "bz2",
	            "mp3", "mp4", "avi", "mkv", "wav", "flac",
	            "exe", "dll", "so", "dylib",
	            "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"
	        };
	        
	        for (String ext : binaryExtensions) {
	            if (ext.equals(extension)) {
	                return true;
	            }
	        }
	    }
	    
	    return false;
	}

	/**
	 * Calculate Shannon entropy of the data
	 */
	private static double calculateEntropy(byte[] data) {
	    int[] byteCounts = new int[256];
	    for (byte b : data) {
	        byteCounts[b & 0xFF]++;
	    }
	    
	    double entropy = 0.0;
	    for (int count : byteCounts) {
	        if (count == 0) continue;
	        double probability = (double) count / data.length;
	        entropy -= probability * (Math.log(probability) / Math.log(2));
	    }
	    return entropy;
	}

	/**
	 * Check if byte distribution is relatively uniform (characteristic of encrypted data)
	 */
	private static boolean hasUniformByteDistribution(byte[] data) {
	    int[] byteCounts = new int[256];
	    for (byte b : data) {
	        byteCounts[b & 0xFF]++;
	    }
	    
	    double expectedCount = (double) data.length / 256;
	    double variance = 0.0;
	    
	    for (int count : byteCounts) {
	        double diff = count - expectedCount;
	        variance += diff * diff;
	    }
	    variance /= 256;
	    
	    double standardDeviation = Math.sqrt(variance);
	    double coefficientOfVariation = standardDeviation / expectedCount;
	    
	    // Encrypted data should have relatively uniform distribution
	    return coefficientOfVariation < 0.5;
	}

	/**
	 * Check if data contains common text patterns
	 */
	private static boolean hasTextPatterns(byte[] data) {
	    if (data.length < 100) return false;
	    
	    int printableChars = 0;
	    int whitespaceChars = 0;
	    int nullBytes = 0;
	    
	    for (byte b : data) {
	        int value = b & 0xFF;
	        if (value == 0) {
	            nullBytes++;
	        } else if ((value >= 32 && value <= 126) || value == 9 || value == 10 || value == 13) {
	            if (value == 32 || value == 9 || value == 10 || value == 13) {
	                whitespaceChars++;
	            }
	            printableChars++;
	        }
	    }
	    
	    double printableRatio = (double) printableChars / data.length;
	    double whitespaceRatio = (double) whitespaceChars / data.length;
	    double nullRatio = (double) nullBytes / data.length;
	    
	    // Text files typically have high printable ratio and some whitespace
	    return printableRatio > 0.7 && whitespaceRatio > 0.05 && nullRatio < 0.1;
	}

	/**
	 * Check if data contains common binary file structures
	 */
	private static boolean hasCommonBinaryStructures(byte[] data) {
	    if (data.length < 50) return false;
	    
	    // Look for patterns common in binary files but rare in encrypted files
	    int nullBytes = 0;
	    int repeatingPatterns = 0;
	    
	    for (int i = 0; i < data.length; i++) {
	        if (data[i] == 0) nullBytes++;
	        
	        // Check for simple repeating patterns
	        if (i >= 4) {
	            boolean isRepeating = true;
	            for (int j = 1; j <= 4 && i - j >= 0; j++) {
	                if (data[i] != data[i - j]) {
	                    isRepeating = false;
	                    break;
	                }
	            }
	            if (isRepeating) repeatingPatterns++;
	        }
	    }
	    
	    double nullRatio = (double) nullBytes / data.length;
	    double repeatingRatio = (double) repeatingPatterns / data.length;
	    
	    // Binary files often have null bytes and some repeating patterns
	    return nullRatio > 0.05 || repeatingRatio > 0.02;
	}
	
	/**
	 * Check if content appears to be Base64 encoded encrypted data
	 */
	private static boolean isBase64EncryptedContent(byte[] data) {
	    try {
	        String content = new String(data, StandardCharsets.UTF_8);
	        
	        // Check if content is valid Base64
	        if (!isValidBase64(content)) {
	            return false;
	        }
	        
	        // Decode Base64 and check if the decoded data has high entropy
	        byte[] decoded = Base64.getDecoder().decode(content.trim());
	        if (decoded.length < 16) return false; // Too small to be meaningful encrypted data
	        
	        // Check entropy of decoded data
	        double entropy = calculateEntropy(decoded);
	        
	        // Check if decoded data has uniform distribution (characteristic of encrypted data)
	        boolean uniformDistribution = hasUniformByteDistribution(decoded);
	        
	        // Relaxed thresholds for Base64 encoded encrypted data
	        // AES encrypted data should have high entropy (>6.5) and reasonably uniform distribution
	        boolean highEntropy = entropy > 6.5; // Lowered from 7.0
	        
	        // Additional check: ensure it's not just Base64 encoded text
	        boolean lacksTextPatterns = !hasTextPatterns(decoded);
	        
	        // For Base64 encrypted content, we need at least high entropy and either uniform distribution OR lack of text patterns
	        return highEntropy && (uniformDistribution || lacksTextPatterns);
	        
	    } catch (Exception e) {
	        return false;
	    }
	}

	/**
	 * Check if string is valid Base64
	 */
	private static boolean isValidBase64(String str) {
	    if (str == null || str.trim().isEmpty()) {
	        return false;
	    }
	    
	    str = str.trim();
	    
	    // Base64 should only contain A-Z, a-z, 0-9, +, /, and = for padding
	    if (!str.matches("^[A-Za-z0-9+/]*={0,2}$")) {
	        return false;
	    }
	    
	    // Length should be multiple of 4
	    if (str.length() % 4 != 0) {
	        return false;
	    }
	    
	    // Should be mostly Base64 characters (at least 95% to be more strict)
	    int base64Chars = 0;
	    for (char c : str.toCharArray()) {
	        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || 
	            (c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=') {
	            base64Chars++;
	        }
	    }
	    
	    double base64Ratio = (double) base64Chars / str.length();
	    return base64Ratio > 0.95; // Increased from 0.9 to be more strict
	}
}